Skip to content

Add judge portal for hackathon submission review#4

Merged
DeeprajPandey merged 39 commits intomainfrom
dev-judging
Feb 3, 2026
Merged

Add judge portal for hackathon submission review#4
DeeprajPandey merged 39 commits intomainfrom
dev-judging

Conversation

@DeeprajPandey
Copy link
Contributor

TL;DR

Adds a judge portal at /judge for reviewing hackathon submissions with two-pass scoring, final decisions, bulk actions, and CSV export.

Note

Access restricted to admins (role 1) and judges (role 7).


DB changes

Migrations:

  • 20260118000002_create_judging_notes.sql - final decisions table
  • 20260118000003_fix_judging_notes.sql - adds moddatetime extension
  • 20260120000001_create_judging_scores.sql - multi-reviewer scores table

Warning

Requires moddatetime PostgreSQL extension. Run migrations before deployment.


Features

  • Split-view UI: team list + scoring panel
  • Two-pass scoring: pass 1 (4 reviewers), pass 2 (3 reviewers)
  • Scoring criteria: problem, solution, implementation, roadmap (1-5)
  • Final decisions: finalist, waitlist, not selected
  • Bulk actions: select teams → mark decision in batch
  • CSV export: finalists, waitlist, not selected
  • Member profiles: view Tally form responses per team
  • Filters: track, hardware, pass status, decision
  • Keyboard nav: j/k to browse

API endpoints

Method Endpoint Purpose
GET /api/judge/submissions List submitted teams with scores
GET /api/judge/export?decision= Export teams as JSON for CSV
POST /api/judge/bulk-decision Mark multiple teams
GET/PUT /api/judge/notes/[teamId] Get/update final decision
GET/POST /api/judge/scores/[teamId] Get/submit reviewer scores
GET /api/judge/tally-profile?teamId= Single team Tally data
POST /api/judge/team-profiles Batch team Tally data

…otes

- Verify team has submitted (status=3) before allowing judging
- Add optimistic locking to prevent concurrent update conflicts
- Return 409 Conflict when another judge has updated the record
- Frontend sends expected_updated_at and handles conflict response
- Extract SDG color mapping to constant to eliminate duplication
- Add unsaved changes confirmation when closing/switching rows
- Add 300ms debounce to search input for better performance
- Fix event handler types with proper ChangeEvent typing
- Remove unused imports (NeoCard, NeoBadge)
Hackathon scale doesn't require pagination - load up to 1000 teams
in one request for simpler client-side filtering.
- Display 25 items per page with prev/next navigation
- Show "Showing X-Y of Z" info
- Reset to page 1 when filters change
- Normalizes scores to one row per judge per team per pass
- Migrates existing judging_notes data as first reviewer entries
- Keeps judging_notes for final_decision (avoids migration complexity)
- Fetches scores from new judging_scores table
- Computes per-reviewer averages and pass aggregates
- Implements consensus logic (majority-based voting)
- Maintains backward compatibility with legacy judgingNotes fields
- PATCH /api/judge/scores/[teamId] for saving reviewer scores
- Enforces reviewer caps (4 for Pass 1, 3 for Pass 2)
- Upserts on (team_id, judge_id, pass) unique constraint
- Returns updated scores and aggregate after save
- Pass 1/2 scores now handled by /api/judge/scores/[teamId]
- Returns helpful error if legacy fields are sent
- Split-view layout: team list (left) + detail panel (right)
- Mobile-responsive with list/detail navigation (<900px breakpoint)
- Multi-reviewer display showing all judges' scores and notes
- Aggregate scores with consensus voting (majority-based)
- "Needs Reviews" filter to find teams below reviewer cap
- Keyboard navigation (↑/↓ or j/k) for efficient judging
- Unsaved changes warning before switching teams
- Move 'Your Review' form above 'Other Reviews' section
- Primary action (scoring) now visible without scrolling
- Filter current user from 'Other Reviews' list (shown separately)
- Sticky save bar at bottom of detail panel, always visible
- Success toast with 2.5s auto-dismiss on save
- Proper timeout cleanup on unmount
- Bar hidden when cap reached and user can't save
- Quick filter buttons for P1/P2 where current judge hasn't scored
- Toggle buttons show active state with blue highlight
- Combines with existing filters using AND logic
- Larger padding, thicker borders, bolder font weight
- Added icons: ✓ Yes, ◐ Maybe/Waitlist, ✗ No, ★ Finalist
- Uppercase text transform for emphasis
- Hover lift effect with shadow animation
- Collapse dropdown filters behind toggle by default
- Keep search and quick filter buttons always visible
- Show active filter count badge when filters applied
- Reduces filter section height when collapsed
- Final tab shows indicator (✓ for decided, — for pending)
- MAYBE/Waitlist buttons use darker orange for better contrast
- Team list items more compact with visual separation
- Filters toggle looks like a clickable button
- HW badge moved before SDG badge in team list
- SDG badges slightly smaller
Project title shown as main heading, team name as "by" subtitle.
Clearer visual hierarchy for judges reviewing submissions.
Allows bulk-marking multiple teams as finalist/waitlist/not_selected
in a single API call instead of updating one at a time.

- POST /api/judge/bulk-decision
- Accepts { teamIds: string[], decision: 'finalist' | 'waitlist' | 'not_selected' }
- Max 500 teams per request
- Auth: SUPERADMIN or JUDGE role required
Export teams and member details (name, email) for finalist
or waitlist teams. Returns JSON for frontend CSV conversion.

- GET /api/judge/export?decision=finalist|waitlist
- Returns team name, project title, and all members
- Auth: SUPERADMIN or JUDGE role required
…dge portal

Streamlines finalist/waitlist selection workflow:

- Select all visible checkbox with indeterminate state
- Bulk action buttons: Mark as Finalist/Waitlist, Clear Selection
- Export buttons: Download CSV with team + member emails
- Track breakdown summary showing teams/people by SDG for finalists/waitlist
- Fix: Allow Final tab access when team already has finalDecision set
- CSV export properly escapes quotes to prevent injection
… teams

- Export not_selected now returns all submitted teams NOT marked as finalist/waitlist
- Added "Mark as Not Selected" bulk action button
- Added "Not Selected CSV" export button
@DeeprajPandey DeeprajPandey merged commit d7d63a9 into main Feb 3, 2026
1 of 4 checks passed
@DeeprajPandey DeeprajPandey deleted the dev-judging branch February 3, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant